package service.synchronization;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import javax.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import dao.IDaoAsof;
import dao.IDaoRef;
import entities.Delegation;
import entities.LinkAssociation;
import entities.OwnerAssociation;
import entities.OwnerAssociationComparator;
import entities.Sheet;
import entities.SmallSheet;
import entities.TreeAssociation;
import entities.TreeAssociationComparator;
import service.configuration.AssociationType;
import service.configuration.IAssociationConfig;
import service.configuration.IBlockConfig;
import service.configuration.IConfiguration;
import service.configuration.IFieldConfig;
import service.configuration.IPersonSheetConfig;
import service.configuration.ISheetConfig;
import service.configuration.NamingUtil;
import service.normalization.INormalizer;
import service.treemanager.ITreeManager;
import service.validation.ISheetValidator;

@Service
@Lazy(value = true)
public class Synchronizer implements ISynchronizer {

	@Resource
	private IDaoRef daoRef;

	@Resource
	private IDaoAsof daoAsof;

	@Resource
	private ISheetValidator sheetValidator;

	@Resource
	private INormalizer normalizer;

	@Resource
	private IConfiguration config;
	
	@Resource
	private ITreeManager treeManager;

	private Map<String, Integer> nbSheetsInAsofBeforeSynchro;
	private Map<String, Integer> nbSheetsAddedInAsof;
	private Map<String, Integer> nbSheetsRemovedInAsof;
	private Map<String, Integer> nbSheetsUpdatedInAsof;

	private int nbOwnerAssociationsInAsofBeforeSynchro;
	private int nbOwnerAssociationsAddedInAsof;
	private int nbOwnerAssociationsRemovedInAsof;
	private int nbTreeAssociationsInAsofBeforeSynchro;
	private int nbTreeAssociationsAddedInAsof;
	private int nbTreeAssociationsRemovedInAsof;
	private int nbLinkAssociationsInAsofBeforeSynchro;
	private int nbLinkAssociationsAddedInAsof;
	private int nbLinkAssociationsRemovedInAsof;

	private void initNbSheets() {
		nbSheetsInAsofBeforeSynchro.clear();
		nbSheetsAddedInAsof.clear();
		nbSheetsRemovedInAsof.clear();
		nbSheetsUpdatedInAsof.clear();
	}
	
	private void initNbAssociations() {
		nbOwnerAssociationsInAsofBeforeSynchro = 0;
		nbOwnerAssociationsAddedInAsof = 0;
		nbOwnerAssociationsRemovedInAsof = 0;
		nbTreeAssociationsInAsofBeforeSynchro = 0;
		nbTreeAssociationsAddedInAsof = 0;
		nbTreeAssociationsRemovedInAsof = 0;
		nbLinkAssociationsInAsofBeforeSynchro = 0;
		nbLinkAssociationsAddedInAsof = 0;
		nbLinkAssociationsRemovedInAsof = 0;
	}
	
	
	public Synchronizer() {
		nbSheetsInAsofBeforeSynchro = new HashMap<String, Integer>();
		nbSheetsAddedInAsof = new HashMap<String, Integer>();
		nbSheetsRemovedInAsof = new HashMap<String, Integer>();
		nbSheetsUpdatedInAsof = new HashMap<String, Integer>();
	}

	public int getNbSheetsInAsofBeforeSynchro(String type) {
		return nbSheetsInAsofBeforeSynchro.get(type);
	}

	public int getNbSheetsAddedInAsof(String type) {
		return nbSheetsAddedInAsof.get(type);
	}

	public int getNbSheetsRemovedInAsof(String type) {
		return nbSheetsRemovedInAsof.get(type);
	}

	public int getNbSheetsUpdatedInAsof(String type) {
		return nbSheetsUpdatedInAsof.get(type);
	}

	public int getNbOwnerAssociationsInAsofBeforeSynchro() {
		return nbOwnerAssociationsInAsofBeforeSynchro;
	}
		
	public int getNbOwnerAssociationsAddedInAsof() {
		return nbOwnerAssociationsAddedInAsof;
	}

	public int getNbOwnerAssociationsRemovedInAsof() {
		return nbOwnerAssociationsRemovedInAsof;
	}

	public int getNbTreeAssociationsInAsofBeforeSynchro() {
		return nbTreeAssociationsInAsofBeforeSynchro;
	}
	
	public int getNbTreeAssociationsAddedInAsof() {
		return nbTreeAssociationsAddedInAsof;
	}

	public int getNbTreeAssociationsRemovedInAsof() {
		return nbTreeAssociationsRemovedInAsof;
	}
	
	public int getNbLinkAssociationsInAsofBeforeSynchro() {
		return nbLinkAssociationsInAsofBeforeSynchro;
	}
	
	public int getNbLinkAssociationsAddedInAsof() {
		return nbLinkAssociationsAddedInAsof;
	}

	public int getNbLinkAssociationsRemovedInAsof() {
		return nbLinkAssociationsRemovedInAsof;
	}

	public void synchronize_OLD() {
		initNbSheets();

		for (ISheetConfig sheetConfig : config.getAllSheetConfigs()) {
			// récupère la liste des fiches pour ce type dans REF et ASOF :
			List<Sheet> sheetsRef = daoRef.findAllSheets(sheetConfig.getName());
			List<Sheet> sheetsAsof = daoAsof.findAllSheets(sheetConfig
					.getName());
			nbSheetsInAsofBeforeSynchro.put(sheetConfig.getName(),
					sheetsAsof.size());

			// construit une map des fiches de ASOF (la clef étant le code des
			// fiches) :
			Map<String, Sheet> sheetsAsofMap = new HashMap<String, Sheet>();
			for (Sheet sheet : sheetsAsof) {
				sheetsAsofMap.put(sheet.getCode(), sheet);
			}

			/*
			 * pour chaque fiche dans REF on regarde si la fiche est dans ASOF
			 * et on l'ajoute si elle n'y est pas encore
			 */
			int nbLinesAddedInAsof = 0;
			List<Sheet> sheetsToAdd = new ArrayList<Sheet>();
			for (Sheet sheet : sheetsRef) {
				if (sheetsAsofMap.get(sheet.getCode()) == null) {

					// normalise tous les champs text de la fiche :
					normalizer.normalizeSheet(sheet);

					// valide tous les champs de la fiche :
					sheetValidator.validate(sheet);

					sheetsToAdd.add(sheet);
					nbLinesAddedInAsof++;
				}
			}
			this.nbSheetsAddedInAsof.put(sheetConfig.getName(),
					nbLinesAddedInAsof);

			daoAsof.addSheets(sheetsToAdd);
		}
	}

	public void synchronizeSheets() {
		initNbSheets();

		for (ISheetConfig sheetConfig : config.getAllSheetConfigs()) {
			synchronizeSheets(sheetConfig);
		}
	}

	public void synchronizeAssociations() {
		
		initNbAssociations();
		
		for(ISheetConfig fromSheetConfig : config.getAllSheetConfigs()) {
			synchronizeOwnerAssociations(fromSheetConfig.getName());
			
			for(ISheetConfig toSheetConfig : config.getAllSheetConfigs()) {

				synchronizeTreeAssociations(fromSheetConfig.getName(),
											toSheetConfig.getName());

				synchronizerLinkAssociations(fromSheetConfig.getName(),
							                 toSheetConfig.getName());		}
		}
		
	}

	
	private void synchronizeOwnerAssociations(String sheetType) {
		
		IAssociationConfig associationConfig = config.getFirstAssociationConfig(
				AssociationType.owner,  config.getPersonType(), 
				sheetType);
		
		if (associationConfig == null) {
			//ownerAssociationTypesToRemove.add(fromSheetConfig.getName());
			List<OwnerAssociation> associations = daoAsof.findOwnerAssociationsBySheetType(
					sheetType);
			nbOwnerAssociationsInAsofBeforeSynchro += associations.size();
			nbOwnerAssociationsRemovedInAsof += associations.size();
			daoAsof.removeOwnerAssociations(associations);
			return;
		}
		
		Comparator<OwnerAssociation> ownerComparator = new OwnerAssociationComparator();			
		List<OwnerAssociation> refAssociations = daoRef.findOwnerAssociations(
				associationConfig.getQuery(), sheetType);
		List<OwnerAssociation> asofAssociations = daoAsof
				.findOwnerAssociationsBySheetType(sheetType);

		nbOwnerAssociationsInAsofBeforeSynchro += asofAssociations.size();
		
		Collections.sort(refAssociations, ownerComparator);
		Collections.sort(asofAssociations, ownerComparator);
		
		List<OwnerAssociation> associationsToAdd = new ArrayList<OwnerAssociation>();
		List<OwnerAssociation> associationsToRemove = new ArrayList<OwnerAssociation>();

		int i = 0, j = 0;
		while ((i < refAssociations.size()) && (j < asofAssociations.size())) {
			OwnerAssociation refAssociation = refAssociations.get(i);
			OwnerAssociation asofAssociation = asofAssociations.get(j);
			int c = ownerComparator.compare(refAssociation, asofAssociation);
			if (c < 0) {
				associationsToAdd.add(refAssociation);
				i++;
			} else if (c > 0) {
				associationsToRemove.add(asofAssociation);
				j++;
			} else {
				i++;
				j++;
			}
		}

		while (i < refAssociations.size()) {
			associationsToAdd.add(refAssociations.get(i++));
		}

		while (j < asofAssociations.size()) {
			associationsToRemove.add(asofAssociations.get(j++));
		}

		nbOwnerAssociationsAddedInAsof += associationsToAdd.size();
		nbOwnerAssociationsRemovedInAsof += associationsToRemove.size();
		
		daoAsof.addOwnerAssociations(associationsToAdd);
		daoAsof.removeOwnerAssociations(associationsToRemove);
	}

	
	private void synchronizeTreeAssociations(String parentType,
			String childType) {
		
		IAssociationConfig associationConfig = config.getFirstAssociationConfig(
				AssociationType.tree, parentType, childType);
		
		if (associationConfig == null) {
			List<TreeAssociation> associations = daoAsof.findTreeAssociationsByTypes(
					parentType, childType);
			nbTreeAssociationsInAsofBeforeSynchro += associations.size();
			nbTreeAssociationsRemovedInAsof += associations.size();
			daoAsof.removeTreeAssociations(associations);
			return;
		} 
		
		Comparator<TreeAssociation> treeComparator = new TreeAssociationComparator();
		List<TreeAssociation> refAssociations = daoRef.findTreeAssociations(
				associationConfig.getQuery(), parentType, childType);
		List<TreeAssociation> asofAssociations = daoAsof
				.findTreeAssociationsByTypes(parentType, childType);

		nbTreeAssociationsInAsofBeforeSynchro += asofAssociations.size();
		
		Collections.sort(refAssociations, treeComparator);
		Collections.sort(asofAssociations, treeComparator);
		
		List<TreeAssociation> associationsToAdd = new ArrayList<TreeAssociation>();
		List<TreeAssociation> associationsToRemove = new ArrayList<TreeAssociation>();

		int i = 0, j = 0;
		while ((i < refAssociations.size()) && (j < asofAssociations.size())) {
			TreeAssociation refAssociation = refAssociations.get(i);
			TreeAssociation asofAssociation = asofAssociations.get(j);
			int c = treeComparator.compare(refAssociation, asofAssociation);
			if (c < 0) {
				associationsToAdd.add(refAssociation);
				i++;
			} else if (c > 0) {
				associationsToRemove.add(asofAssociation);
				j++;
			} else {
				i++;
				j++;
			}
		}

		while (i < refAssociations.size()) {
			associationsToAdd.add(refAssociations.get(i++));
		}

		while (j < asofAssociations.size()) {
			associationsToRemove.add(asofAssociations.get(j++));
		}

		nbTreeAssociationsAddedInAsof += associationsToAdd.size();
		nbTreeAssociationsRemovedInAsof += associationsToRemove.size();
		
		daoAsof.addTreeAssociations(associationsToAdd);
		daoAsof.removeTreeAssociations(associationsToRemove);
	}

	
	private void synchronizerLinkAssociations(String fromType, String toType) {
		
		nbLinkAssociationsRemovedInAsof += daoAsof.removeLinkAssociation(fromType, toType);
		
		List<IAssociationConfig> associationConfigs = config.getAssociationConfigs(
				AssociationType.link,  fromType, toType);
		
		if (associationConfigs == null) {
			return;
		}
		
		
		List<LinkAssociation> associations = new ArrayList<LinkAssociation>();
		for (IAssociationConfig associationConfig : associationConfigs) {
				
			associations.addAll(daoRef.findLinkAssociations(
				associationConfig.getQuery(), 
				associationConfig.getFrom(), associationConfig.getTo(),
				associationConfig.getLabel()));
		}
		
		nbLinkAssociationsAddedInAsof += associations.size();
		
		daoAsof.addLinkAssociation(associations);
	}

	
	private void removeSheets(List<String> codesToRemove, String type) {
		if (codesToRemove.isEmpty()) {
			return;
		}

		daoAsof.removeSheets(daoAsof.findSheetsByProperty(NamingUtil.CODE,
				codesToRemove, type));
	}

	
	private void addSheets(List<String> codesToAdd, String type) {
		if (codesToAdd.isEmpty()) {
			return;
		}

		List<Sheet> sheetsToAdd = daoRef.findSheetsByProperty(NamingUtil.CODE,
				codesToAdd, type);

		for (Sheet sheetToAdd : sheetsToAdd) {
			normalizer.normalizeSheet(sheetToAdd);
			sheetValidator.validate(sheetToAdd);
		}

		daoAsof.addSheets(sheetsToAdd);
	}

	
	private void updateSheets(List<String> codesToUpdate, String type,
			ISheetConfig sheetConfig) {
		if (codesToUpdate.isEmpty()) {
			return;
		}

		List<Sheet> refSheetsToUpdate = daoRef.findSheetsByProperty(
				NamingUtil.CODE, codesToUpdate, type);
		Map<String, Sheet> refSheetsToUpdateByCode = new HashMap<String, Sheet>();
		for (Sheet sheet : refSheetsToUpdate) {
			refSheetsToUpdateByCode.put(sheet.getCode(), sheet);
		}

		List<Sheet> asofSheetsToUpdate = daoAsof.findSheetsByProperty(
				NamingUtil.CODE, codesToUpdate, type);

		for (IBlockConfig block : sheetConfig.getBlockConfigs()) {
			for (IFieldConfig field : block.getFieldConfigs()) {
				if (field.isReadOnly()) {
					for (Sheet asofSheet : asofSheetsToUpdate) {
						Sheet refSheet = refSheetsToUpdateByCode.get(asofSheet
								.getCode());
						asofSheet.setProperty(field.getName(),
								refSheet.getProperty(field.getName()));
					}
				}
			}
		}

		for (Sheet asofSheet : asofSheetsToUpdate) {
			Sheet refSheet = refSheetsToUpdateByCode.get(asofSheet.getCode());
			asofSheet.setProperty(NamingUtil.VERSION,
					refSheet.getProperty(NamingUtil.VERSION));
			normalizer.normalizeSheet(asofSheet);
			sheetValidator.validate(asofSheet);
		}

		daoAsof.updateSheets(asofSheetsToUpdate);
	}

	
	private void synchronizeSheets(ISheetConfig sheetConfig) {
		String type = sheetConfig.getName();

		List<SmallSheet> refSheets = daoRef.findAllSmallSheets(type);
		List<SmallSheet> asofSheets = daoAsof.findAllSmallSheets(type);

		nbSheetsInAsofBeforeSynchro
				.put(sheetConfig.getName(), asofSheets.size());

		List<String> codesToAdd = new ArrayList<String>();
		List<String> codesToRemove = new ArrayList<String>();
		List<String> codesToUpdate = new ArrayList<String>();

		int i = 0, j = 0;
		while ((i < refSheets.size()) && (j < asofSheets.size())) {
			SmallSheet refSheet = refSheets.get(i);
			SmallSheet asofSheet = asofSheets.get(j);
			int c = refSheet.getCode().compareToIgnoreCase(asofSheet.getCode());
			if (c == 0) {
				if (!refSheet.getVersion().equals(asofSheet.getVersion())) {
					codesToUpdate.add(refSheet.getCode());
				}
				i++;
				j++;
			} else if (c < 0) {
				codesToAdd.add(refSheet.getCode());
				i++;
			} else {
				codesToRemove.add(asofSheet.getCode());
				j++;
			}
		}

		while (i < refSheets.size()) {
			codesToAdd.add(refSheets.get(i++).getCode());
		}

		while (j < asofSheets.size()) {
			codesToRemove.add(asofSheets.get(j++).getCode());
		}

		nbSheetsAddedInAsof.put(sheetConfig.getName(), codesToAdd.size());
		nbSheetsRemovedInAsof.put(sheetConfig.getName(), codesToRemove.size());
		nbSheetsUpdatedInAsof.put(sheetConfig.getName(), codesToUpdate.size());

		addSheets(codesToAdd, type);
		removeSheets(codesToRemove, type);
		updateSheets(codesToUpdate, type, sheetConfig);
	}


	public void synchronizeAssociations_bouchon() {
		List<Sheet> allSheets = new ArrayList<Sheet>();
		List<OwnerAssociation> ownerAssociations = new ArrayList<OwnerAssociation>();
		List<Delegation> delegations = new ArrayList<Delegation>();

		for (ISheetConfig sheetConfig : config.getConfig().getSheetConfigs()) {
			List<Sheet> sheets = daoAsof.findAllSheets(sheetConfig.getName());
			allSheets.addAll(sheets);
		}

		IPersonSheetConfig personSheetConfig = config.getConfig()
				.getPersonSheetConfig();

		Random random = new Random();
		List<Sheet> persons = daoAsof
				.findAllSheets(personSheetConfig.getName());

		// on supprime tous les liens de propriété et d'associations de ASOF :
		daoAsof.removeAllDelegations();
		daoAsof.removeAllOwnerAssociations();

		// pour chaque individu
		for (Sheet owner : persons) {
			if (random.nextInt(4) <= 2)
				continue;

			// on génère un nombre aléatoire de liens de propriété
			int randomNbOwnerships = random.nextInt(5);
			Set<String> ownedSheetCodes = new TreeSet<String>();
			for (int i = 0; i < randomNbOwnerships; i++) {
				int randomSheet = random.nextInt(allSheets.size());
				if (!ownedSheetCodes.contains(allSheets.get(randomSheet)
						.getCode())) {
					OwnerAssociation ownerAssociation = new OwnerAssociation();
					ownerAssociation.setPersonCode(owner.getCode());
					ownerAssociation.setSheetCode(allSheets.get(randomSheet)
							.getCode());
					ownerAssociation.setSheetType(allSheets.get(randomSheet)
							.getType());

					ownerAssociations.add(ownerAssociation);
					ownedSheetCodes.add(allSheets.get(randomSheet).getCode());

					/*
					 * pour chaque lien de propriété on génère un nb aléatoire
					 * de liens de délégations
					 */
					int randomNbDelegations = random.nextInt(15);
					Set<String> delegatedPersonCodes = new TreeSet<String>();
					for (int j = 0; j < randomNbDelegations; j++) {
						int randomPerson = random.nextInt(persons.size());

						/*
						 * on choisit au hasard une person à qui déléguer la
						 * fiche (un individu ne se delegue pas à lui même)
						 */
						if ((persons.get(randomPerson) != owner)
								&& !delegatedPersonCodes.contains(persons.get(
										randomSheet).getCode())) {

							Delegation delegation = new Delegation();
							delegation.setOwnerCode(owner.getCode());
							delegation.setPersonCode(persons.get(randomPerson)
									.getCode());
							delegation.setSheetCode(allSheets.get(randomSheet)
									.getCode());
							delegation.setSheetType(allSheets.get(randomSheet)
									.getType());

							delegations.add(delegation);
							delegatedPersonCodes.add(persons.get(randomPerson)
									.getCode());
						}
					}
				}
			}
		}

		daoAsof.addOwnerAssociations(ownerAssociations);
		daoAsof.addDelegations(delegations);
	}


	public void addRandomDelegations() {

		Set<String> delegatedPersonCodes = new TreeSet<String>();
		List<Delegation> delegations = new ArrayList<Delegation>();
		List<Sheet> allSheets = new ArrayList<Sheet>();
		Random random = new Random();

		List<Sheet> persons = daoAsof
				.findAllSheets(config.getPersonType());
		
		for (ISheetConfig sheetConfig : config.getConfig().getSheetConfigs()) {
			List<Sheet> sheets = daoAsof.findAllSheets(sheetConfig.getName());
			allSheets.addAll(sheets);
		}

		// on supprime tous les liens de propriété et d'associations de ASOF :
		daoAsof.removeAllDelegations();

		List<OwnerAssociation> ownerAssociations = daoAsof.findOwnerAssociations();
		
		for(OwnerAssociation ownerAssociation : ownerAssociations) {
			if (random.nextInt(4) <= 2)
				continue;
			
			/*
			 * pour chaque lien de propriété on génère un nb aléatoire
			 * de liens de délégations
			 */
			int randomNbDelegations = random.nextInt(20);
			
			for (int j = 0; j < randomNbDelegations; j++) {
				int randomPerson = random.nextInt(persons.size());

				/*
				 * on choisit au hasard une person à qui déléguer la
				 * fiche (un individu ne se delegue pas à lui même)
				 */
				if ((ownerAssociation.getPersonCode() != persons.get(randomPerson).getCode())
						&& !delegatedPersonCodes.contains(ownerAssociation.getPersonCode() + ownerAssociation.getSheetCode() + randomPerson)) {

					Delegation delegation = new Delegation();
					delegation.setOwnerCode(ownerAssociation.getPersonCode());
					delegation.setPersonCode(persons.get(randomPerson)
							.getCode());
					delegation.setSheetCode(ownerAssociation.getSheetCode());
					delegation.setSheetType(ownerAssociation.getSheetType());
					
					delegations.add(delegation);

					delegatedPersonCodes.add(ownerAssociation.getPersonCode() + ownerAssociation.getSheetCode() + randomPerson);
				}
			}
		}

		daoAsof.addDelegations(delegations);
	}
	
	
	public void callSynchronisationListeners() {
		treeManager.generateTree();
	}

}
